/*! * * ZSSRichTextEditor v1.0 * http://www.zedsaid.com * * Copyright 2013 Zed Said Studio * */ function callObjc(url) { var iframe = document.createElement("IFRAME"); iframe.setAttribute("src", url); iframe.style.cssText = "border: 0px transparent;"; document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null; } var log = function(o) { console.log(o); callObjc('callback-log:msg=' + o); } var LEAMD = {}; // If we are using iOS or desktop var isUsingiOS = true; // THe default callback parameter separator var defaultCallbackSeparator = '~'; const NodeName = { BLOCKQUOTE: "BLOCKQUOTE", PARAGRAPH: "P" }; // The editor object var ZSSEditor = {}; // These variables exist to reduce garbage (as in memory garbage) generation when typing real fast // in the editor. // ZSSEditor.caretArguments = ['yOffset=' + 0, 'height=' + 0]; ZSSEditor.caretInfo = { y: 0, height: 0 }; // Is this device an iPad ZSSEditor.isiPad; // The current selection ZSSEditor.currentSelection; // The current editing image ZSSEditor.currentEditingImage; // The current editing video ZSSEditor.currentEditingVideo; // The current editing link ZSSEditor.currentEditingLink; ZSSEditor.focusedField = null; // The objects that are enabled ZSSEditor.enabledItems = {}; ZSSEditor.editableFields = {}; ZSSEditor.lastTappedNode = null; // The default paragraph separator ZSSEditor.defaultParagraphSeparator = 'p'; /** * The initializer function that must be called onLoad */ ZSSEditor.init = function(callbacker, logger) { ZSSEditor.callbacker = callbacker; ZSSEditor.logger = logger; rangy.init(); // Change a few CSS values if the device is an iPad ZSSEditor.isiPad = (navigator.userAgent.match(/iPad/i) != null); if (ZSSEditor.isiPad) { $(document.body).addClass('ipad-body'); // $('#zss_field_title').addClass('ipad_field_title'); // $('#zss_field_content').addClass('ipad_field_content'); } document.execCommand('insertBrOnReturn', false, false); document.execCommand('defaultParagraphSeparator', false, this.defaultParagraphSeparator); document.execCommand('styleWithCSS', false, false); var editor = $('div.field').each(function() { var editableField = new ZSSField($(this), ZSSEditor.callbacker); var editableFieldId = editableField.getNodeId(); ZSSEditor.editableFields[editableFieldId] = editableField; ZSSEditor.callback("callback-new-field", "id=" + editableFieldId); }); document.addEventListener("selectionchange", function(e) { ZSSEditor.currentEditingLink = null; // DRM: only do something here if the editor has focus. The reason is that when the // selection changes due to the editor loosing focus, the focusout event will not be // sent if we try to load a callback here. // if (editor.is(":focus")) { ZSSEditor.selectionChangedCallback(); ZSSEditor.sendEnabledStyles(e); var clicked = $(e.target); if (!clicked.hasClass('zs_active')) { $('img').removeClass('zs_active'); } } }, false); $('[contenteditable]').on('paste',function(e) { // Ensure we only insert plaintext from the pasteboard e.preventDefault(); var plainText = (e.originalEvent || e).clipboardData.getData('text/plain'); document.execCommand('insertText', false, plainText); }); this.domLoadedCallback(); }; //end // MARK: - Callbacks ZSSEditor.callback = function(callbackScheme, callbackPath) { this.callbacker.callback(callbackScheme, callbackPath); }; ZSSEditor.domLoadedCallback = function() { this.callback("callback-dom-loaded"); }; ZSSEditor.selectionChangedCallback = function () { var joinedArguments = this.getJoinedFocusedFieldIdAndCaretArguments(); this.callback('callback-selection-changed', joinedArguments); this.callback("callback-input", joinedArguments); }; ZSSEditor.stylesCallback = function(stylesArray) { var stylesString = ''; if (stylesArray.length > 0) { stylesString = stylesArray.join(defaultCallbackSeparator); } // log('需要enable的btn:'); // log(stylesString); this.callback("callback-selection-style", stylesString); }; // MARK: - Logging ZSSEditor.log = function(msg) { this.logger.log(msg); }; // MARK: - Debugging logs ZSSEditor.logMainElementSizes = function() { msg = 'Window [w:' + $(window).width() + '|h:' + $(window).height() + ']'; this.log(msg); var msg = encodeURIComponent('Viewport [w:' + window.innerWidth + '|h:' + window.innerHeight + ']'); this.log(msg); msg = encodeURIComponent('Body [w:' + $(document.body).width() + '|h:' + $(document.body).height() + ']'); this.log(msg); msg = encodeURIComponent('HTML [w:' + $('html').width() + '|h:' + $('html').height() + ']'); this.log(msg); msg = encodeURIComponent('Document [w:' + $(document).width() + '|h:' + $(document).height() + ']'); this.log(msg); }; // MARK: - Viewport Refreshing ZSSEditor.refreshVisibleViewportSize = function() { $(document.body).css('min-height', window.innerHeight + 'px'); $('#zss_field_content').css('min-height', (window.innerHeight - $('#zss_field_content').position().top) + 'px'); }; // MARK: - Fields ZSSEditor.focusFirstEditableField = function() { $('div[contenteditable=true]:first').focus(); }; ZSSEditor.getField = function(fieldId) { var field = this.editableFields[fieldId]; return field; }; ZSSEditor.getFocusedField = function() { var currentField = $(this.closerParentNodeWithName('div')); var currentFieldId = currentField.attr('id'); while (currentField && (!currentFieldId || this.editableFields[currentFieldId] == null)) { currentField = this.closerParentNodeStartingAtNode('div', currentField); if(currentField) { currentFieldId = currentField.attr('id'); } } // 这么恶心 var field = this.editableFields[currentFieldId]; if (!field) { return this.editableFields['zss_field_content']; } return field; }; // MARK: - Selection ZSSEditor.backupRange = function(){ var selection = window.getSelection(); var range = selection.getRangeAt(0); ZSSEditor.currentSelection = { "startContainer": range.startContainer, "startOffset": range.startOffset, "endContainer": range.endContainer, "endOffset": range.endOffset }; }; ZSSEditor.restoreRange = function(){ if (this.currentSelection) { var selection = window.getSelection(); selection.removeAllRanges(); var range = document.createRange(); range.setStart(this.currentSelection.startContainer, this.currentSelection.startOffset); range.setEnd(this.currentSelection.endContainer, this.currentSelection.endOffset); selection.addRange(range); } }; ZSSEditor.getSelectedText = function() { var selection = window.getSelection(); return selection.toString(); }; ZSSEditor.getCaretArguments = function() { var caretInfo = this.getYCaretInfo(); if (caretInfo == null) { return null; } else { this.caretArguments[0] = 'yOffset=' + caretInfo.y; this.caretArguments[1] = 'height=' + caretInfo.height; return this.caretArguments; } }; ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments = function() { var joinedArguments = ZSSEditor.getJoinedCaretArguments(); var field = ZSSEditor.getFocusedField(); var id; if(field) { id = field.getNodeId(); } else { id = 'zss_field_content'; } var idArgument = "id=" + id; joinedArguments = idArgument + defaultCallbackSeparator + joinedArguments; return joinedArguments; }; ZSSEditor.getJoinedCaretArguments = function() { var caretArguments = this.getCaretArguments(); var joinedArguments = this.caretArguments.join(defaultCallbackSeparator); return joinedArguments; }; ZSSEditor.getCaretYPosition = function() { var selection = window.getSelection(); var range = selection.getRangeAt(0); var span = document.createElement("span"); // Ensure span has dimensions and position by // adding a zero-width space character span.appendChild( document.createTextNode("\u200b") ); range.insertNode(span); var y = span.offsetTop; var spanParent = span.parentNode; spanParent.removeChild(span); // Glue any broken text nodes back together spanParent.normalize(); return y; } ZSSEditor.getYCaretInfo = function() { var selection = window.getSelection(); var noSelectionAvailable = selection.rangeCount == 0; if (noSelectionAvailable) { return null; } var y = 0; var height = 0; var range = selection.getRangeAt(0); var needsToWorkAroundNewlineBug = (range.getClientRects().length == 0); // PROBLEM: iOS seems to have problems getting the offset for some empty nodes and return // 0 (zero) as the selection range top offset. // // WORKAROUND: To fix this problem we use a different method to obtain the Y position instead. // if (needsToWorkAroundNewlineBug) { var closerParentNode = ZSSEditor.closerParentNode(); var closerDiv = ZSSEditor.closerParentNodeWithName('div'); var fontSize = $(closerParentNode).css('font-size'); var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5); y = this.getCaretYPosition(); height = lineHeight; } else { if (range.getClientRects) { var rects = range.getClientRects(); if (rects.length > 0) { // PROBLEM: some iOS versions differ in what is returned by getClientRects() // Some versions return the offset from the page's top, some other return the // offset from the visible viewport's top. // // WORKAROUND: see if the offset of the body's top is ever negative. If it is // then it means that the offset we have is relative to the body's top, and we // should add the scroll offset. // var addsScrollOffset = document.body.getClientRects()[0].top < 0; if (addsScrollOffset) { y = document.body.scrollTop; } y += rects[0].top; height = rects[0].height; } } } this.caretInfo.y = y; this.caretInfo.height = height; return this.caretInfo; }; // MARK: - Default paragraph separator ZSSEditor.defaultParagraphSeparatorTag = function() { return '<' + this.defaultParagraphSeparator + '>'; }; // MARK: - Styles ZSSEditor.setBold = function() { document.execCommand('bold', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setItalic = function() { document.execCommand('italic', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setSubscript = function() { document.execCommand('subscript', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setSuperscript = function() { document.execCommand('superscript', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setStrikeThrough = function() { var commandName = 'strikeThrough'; var isDisablingStrikeThrough = ZSSEditor.isCommandEnabled(commandName); document.execCommand(commandName, false, null); // DRM: WebKit has a problem disabling strikeThrough when the tag is used instead of // . The code below serves as a way to fix this issue. // var mustHandleWebKitIssue = (isDisablingStrikeThrough && ZSSEditor.isCommandEnabled(commandName)); if (mustHandleWebKitIssue) { var troublesomeNodeNames = ['del']; var selection = window.getSelection(); var range = selection.getRangeAt(0).cloneRange(); var container = range.commonAncestorContainer; var nodeFound = false; var textNode = null; while (container && !nodeFound) { nodeFound = (container && container.nodeType == document.ELEMENT_NODE && troublesomeNodeNames.indexOf(container.nodeName.toLowerCase()) > -1); if (!nodeFound) { container = container.parentElement; } } if (container) { var newObject = $(container).replaceWith(container.innerHTML); var finalSelection = window.getSelection(); var finalRange = selection.getRangeAt(0).cloneRange(); finalRange.setEnd(finalRange.startContainer, finalRange.startOffset + 1); selection.removeAllRanges(); selection.addRange(finalRange); } } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setUnderline = function() { document.execCommand('underline', false, null); ZSSEditor.sendEnabledStyles(); }; /** * @brief Turns blockquote ON or OFF for the current selection. * @details This method makes sure that the contents of the blockquotes are surrounded by the * defaultParagraphSeparatorTag (by default '

'). This ensures parity with the web * editor. */ ZSSEditor.setBlockquote = function() { var savedSelection = rangy.saveSelection(); var selection = document.getSelection(); var range = selection.getRangeAt(0).cloneRange(); var sendStyles = false; var ancestorElement = this.getAncestorElementForSettingBlockquote(range); if (ancestorElement) { sendStyles = true; var childNodes = this.getChildNodesIntersectingRange(ancestorElement, range); if (childNodes && childNodes.length) { this.toggleBlockquoteForSpecificChildNodes(ancestorElement, childNodes); } } rangy.restoreSelection(savedSelection); if (sendStyles) { ZSSEditor.sendEnabledStyles(); } }; function removeFomat() { try { // 如果没有, 则到cursor所在的span或p下 var selection = window.getSelection(); var range = selection.getRangeAt(0).cloneRange(); var curNode = range.commonAncestorContainer; // 找到p while(true) { var nodeName = curNode.nodeName.toLowerCase(); if(nodeName == "p" || nodeName == 'div' || nodeName == 'hr' || nodeName == 'table' || nodeName == 'img' || nodeName == 'a' || nodeName == 'span' || nodeName == 'font' || nodeName == 'body' || nodeName.indexOf('h') === 0 || nodeName == 'blockquote') { break; } curNode = curNode.parentNode; } if(nodeName === 'img') { return; } var $node = $(curNode); if(nodeName != "body" && nodeName != "html") { if(nodeName == 'div' && curNode.className == 'zss_field_content') { return; } var replaceNodeName = nodeName; // 如果是heading则替换之 if(nodeName.indexOf('h') != -1 || nodeName == 'blockquote') { replaceNodeName = 'p'; } else if(nodeName === 'a') { replaceNodeName = 'span'; } var tokenId = 'LEACLR-' + (new Date()).getTime(); var t = '~*Z+&'; var tSpan = ''; var tokenSpan = $(tSpan).get(0); var replacedText; if($node.find('img').length > 0) { range.insertNode(tokenSpan); replacedText = $node.html(); } else { tokenSpan.innerHTML = t; range.insertNode(tokenSpan); replacedText = $node.text(); replacedText = replacedText.replace(t, tSpan); } // 替换之 $node.replaceWith('<' + replaceNodeName + '>' + replacedText + '') // move cursor到span下 var range = document.createRange(); var sel = window.getSelection(); range.setStart($('#' + tokenId).get(0), 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); // 可能有问题 $('#' + tokenId).remove(); } console.log('format ok'); } catch(e) { console.error(e); } } ZSSEditor.removeFormating = function() { // 选区是否有文字 var selectedText = window.getSelection().toString(); if(selectedText) { document.execCommand('removeFormat', false, null); } else { removeFomat(); } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setHorizontalRule = function() { document.execCommand('insertHorizontalRule', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setHeading = function(heading) { var formatTag = heading; var formatBlock = document.queryCommandValue('formatBlock'); if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) { document.execCommand('formatBlock', false, this.defaultParagraphSeparatorTag()); } else { document.execCommand('formatBlock', false, '<' + formatTag + '>'); } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setParagraph = function() { var formatTag = "p"; var formatBlock = document.queryCommandValue('formatBlock'); if (formatBlock.length > 0 && formatBlock.toLowerCase() == formatTag) { document.execCommand('formatBlock', false, this.defaultParagraphSeparatorTag()); } else { document.execCommand('formatBlock', false, '<' + formatTag + '>'); } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.undo = function() { document.execCommand('undo'); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.redo = function() { document.execCommand('redo'); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setOrderedList = function() { document.execCommand('insertOrderedList', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setUnorderedList = function() { document.execCommand('insertUnorderedList', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setJustifyCenter = function() { document.execCommand('justifyCenter', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setJustifyFull = function() { document.execCommand('justifyFull', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setJustifyLeft = function() { document.execCommand('justifyLeft', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setJustifyRight = function() { document.execCommand('justifyRight', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setIndent = function() { document.execCommand('indent', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setOutdent = function() { document.execCommand('outdent', false, null); ZSSEditor.sendEnabledStyles(); }; ZSSEditor.setTextColor = function(color) { ZSSEditor.restoreRange(); document.execCommand("styleWithCSS", null, true); document.execCommand('foreColor', false, color); document.execCommand("styleWithCSS", null, false); ZSSEditor.sendEnabledStyles(); // document.execCommand("removeFormat", false, "foreColor"); // Removes just foreColor }; ZSSEditor.setBackgroundColor = function(color) { ZSSEditor.restoreRange(); document.execCommand("styleWithCSS", null, true); document.execCommand('hiliteColor', false, color); document.execCommand("styleWithCSS", null, false); ZSSEditor.sendEnabledStyles(); }; // Needs addClass method ZSSEditor.insertLink = function(url, title) { ZSSEditor.restoreRange(); var sel = document.getSelection(); if (sel.rangeCount) { var el = document.createElement("a"); el.setAttribute("href", url); var range = sel.getRangeAt(0).cloneRange(); range.surroundContents(el); el.innerHTML = title; sel.removeAllRanges(); sel.addRange(range); } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.updateLink = function(url, title) { ZSSEditor.restoreRange(); var currentLinkNode = ZSSEditor.lastTappedNode; if (currentLinkNode) { currentLinkNode.setAttribute("href", url); currentLinkNode.innerHTML = title; } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.unlink = function() { var currentLinkNode = ZSSEditor.closerParentNodeWithName('a'); if (currentLinkNode) { this.unwrapNode(currentLinkNode); } ZSSEditor.sendEnabledStyles(); }; ZSSEditor.unwrapNode = function(node) { var savedSelection = rangy.saveSelection(); $(node).contents().unwrap(); rangy.restoreSelection(savedSelection); }; ZSSEditor.wrapNode = function(node) { var savedSelection = rangy.saveSelection(); $(node).contents().wrap("

"); rangy.restoreSelection(savedSelection); }; ZSSEditor.quickLink = function() { var sel = document.getSelection(); var link_url = ""; var test = new String(sel); var mailregexp = new RegExp("^(.+)(\@)(.+)$", "gi"); if (test.search(mailregexp) == -1) { checkhttplink = new RegExp("^http\:\/\/", "gi"); if (test.search(checkhttplink) == -1) { checkanchorlink = new RegExp("^\#", "gi"); if (test.search(checkanchorlink) == -1) { link_url = "http://" + sel; } else { link_url = sel; } } else { link_url = sel; } } else { checkmaillink = new RegExp("^mailto\:", "gi"); if (test.search(checkmaillink) == -1) { link_url = "mailto:" + sel; } else { link_url = sel; } } var html_code = '' + sel + ''; ZSSEditor.insertHTML(html_code); }; // MARK: - Blockquotes /** * @brief This method toggles blockquote for the specified child nodes. This is useful since * we can toggle blockquote either for some or ALL of the child nodes, depending on * what we need to achieve. * @details CASE 1: If the parent node is a blockquote node, the child nodes will be extracted * from it leaving the remaining siblings untouched (by splitting the parent blockquote * node in two if necessary). * CASE 2: If the parent node is NOT a blockquote node, but the first child is, the * method will make sure all child nodes that are blockquote nodes will be toggled to * non-blockquote nodes. * CASE 3: If both the parent node and the first node are non-blockquote nodes, this * method will turn all child nodes into blockquote nodes. * * @param parentNode The parent node. Can be either a blockquote or non-blockquote node. * Cannot be null. * @param nodes The child nodes. Can be any combination of blockquote and * non-blockquote nodes. Cannot be null. */ ZSSEditor.toggleBlockquoteForSpecificChildNodes = function(parentNode, nodes) { if (nodes && nodes.length > 0) { if (parentNode.nodeName == NodeName.BLOCKQUOTE) { for (var counter = 0; counter < nodes.length; counter++) { this.turnBlockquoteOffForNode(nodes[counter]); } } else { var turnOn = (nodes[0].nodeName != NodeName.BLOCKQUOTE); for (var counter = 0; counter < nodes.length; counter++) { if (turnOn) { this.turnBlockquoteOnForNode(nodes[counter]); } else { this.turnBlockquoteOffForNode(nodes[counter]); } } } } }; /** * @brief Turns blockquote off for the specified node. * * @param node The node to turn the blockquote off for. It can either be a blockquote * node (in which case it will be removed and all child nodes extracted) or * have a parent blockquote node (in which case the node will be extracted * from its parent). */ ZSSEditor.turnBlockquoteOffForNode = function(node) { if (node.nodeName == NodeName.BLOCKQUOTE) { for (var i = 0; i < node.childNodes.length; i++) { this.extractNodeFromAncestorNode(node.childNodes[i], node); } } else { if (node.parentNode.nodeName == NodeName.BLOCKQUOTE) { this.extractNodeFromAncestorNode(node, node.parentNode); } } }; /** * @brief Turns blockquote on for the specified node. * * @param node The node to turn blockquote on for. Will attempt to attach the newly * created blockquote to sibling or uncle blockquote nodes. If the node is * null or it's parent is null, this method will exit without affecting it * (this can actually be caused by this method modifying the surrounding * nodes, if those nodes are stored in an array - and thus are not notified * of DOM hierarchy changes). */ ZSSEditor.turnBlockquoteOnForNode = function(node) { if (!node || !node.parentNode) { return; } var couldJoinBlockquotes = this.joinAdjacentSiblingsOrAncestorBlockquotes(node); if (!couldJoinBlockquotes) { var blockquote = document.createElement(NodeName.BLOCKQUOTE); node.parentNode.insertBefore(blockquote, node); blockquote.appendChild(node); } }; // MARK: - Images ZSSEditor.updateImage = function(url, alt) { ZSSEditor.restoreRange(); if (ZSSEditor.currentEditingImage) { var c = ZSSEditor.currentEditingImage; c.attr('src', url); c.attr('alt', alt); } ZSSEditor.sendEnabledStyles(); }; /* ZSSEditor.insertImage = function(url, alt) { url = url[0]; var html = ''+alt+''; // alert(html); try { this.insertHTML(html); this.sendEnabledStyles(); } catch(e) { console.log(e); // alert(e + ''); } }; */ ZSSEditor.insertImage = function(urls, alt) { ZSSEditor.restoreRange(); if(typeof urls == 'string') { urls = [urls]; } for(var i = 0; i < urls.length; ++i) { var url = urls[i]; // console.log(url); var html = ''+alt+''; // console.log(html); try { this.insertHTML(html); } catch(e) { console.error(e); } } // this.sendEnabledStyles(); }; /** * @brief Inserts a local image URL. Useful for images that need to be uploaded. * @details By inserting a local image URL, we can make sure the image is shown to the user * as soon as it's selected for uploading. Once the image is successfully uploaded * the application should call replaceLocalImageWithRemoteImage(). * * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as * a mechanism to update the image node with the remote URL * when replaceLocalImageWithRemoteImage() is called. * @param localImageUrl The URL of the local image to display. Please keep in mind * that a remote URL can be used here too, since this method * does not check for that. It would be a mistake. */ ZSSEditor.insertLocalImage = function(imageNodeIdentifier, localImageUrl) { var space = ' '; var progressIdentifier = this.getImageProgressIdentifier(imageNodeIdentifier); var imageContainerIdentifier = this.getImageContainerIdentifier(imageNodeIdentifier); var imgContainerStart = ''; var imgContainerEnd = ''; var progress = ''; var image = ''; var html = imgContainerStart + progress+image + imgContainerEnd; html = space + html + space; this.insertHTML(html); this.sendEnabledStyles(); }; ZSSEditor.getImageNodeWithIdentifier = function(imageNodeIdentifier) { return $('img[data-wpid="' + imageNodeIdentifier+'"]'); }; ZSSEditor.getImageProgressIdentifier = function(imageNodeIdentifier) { return 'progress_' + imageNodeIdentifier; }; ZSSEditor.getImageProgressNodeWithIdentifier = function(imageNodeIdentifier) { return $('#'+this.getImageProgressIdentifier(imageNodeIdentifier)); }; ZSSEditor.getImageContainerIdentifier = function(imageNodeIdentifier) { return 'img_container_' + imageNodeIdentifier; }; ZSSEditor.getImageContainerNodeWithIdentifier = function(imageNodeIdentifier) { return $('#'+this.getImageContainerIdentifier(imageNodeIdentifier)); }; ZSSEditor.isMediaContainerNode = function(node) { if (node.id === undefined) { return false; } return (node.id.search("img_container_") == 0) || (node.id.search("video_container_") == 0); }; ZSSEditor.extractMediaIdentifier = function(node) { if (node.id.search("img_container_") == 0) { return node.id.replace("img_container_", ""); } else if (node.id.search("video_container_") == 0) { return node.id.replace("video_container_", ""); } return ""; }; /** * @brief Replaces a local image URL with a remote image URL. Useful for images that have * just finished uploading. * @details The remote image can be available after a while, when uploading images. This method * allows for the remote URL to be loaded once the upload completes. * * @param imageNodeIdentifier This is a unique ID provided by the caller. It exists as * a mechanism to update the image node with the remote URL * when replaceLocalImageWithRemoteImage() is called. * @param remoteImageUrl The URL of the remote image to display. */ ZSSEditor.replaceLocalImageWithRemoteImage = function(imageNodeIdentifier, remoteImageUrl) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length == 0) { // even if the image is not present anymore we must do callback this.markImageUploadDone(imageNodeIdentifier); return; } var image = new Image; image.onload = function () { imageNode.attr('src', image.src); ZSSEditor.markImageUploadDone(imageNodeIdentifier); } image.onerror = function () { // Even on an error, we swap the image for the time being. This is because private // blogs are currently failing to download images due to access privilege issues. // imageNode.attr('src', image.src); ZSSEditor.markImageUploadDone(imageNodeIdentifier); } image.src = remoteImageUrl; }; /** * @brief Update the progress indicator for the image identified with the value in progress. * * @param imageNodeIdentifier This is a unique ID provided by the caller. * @param progress A value between 0 and 1 indicating the progress on the image. */ ZSSEditor.setProgressOnImage = function(imageNodeIdentifier, progress) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length == 0){ return; } if (progress < 1){ imageNode.addClass("uploading"); } var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier); if (imageProgressNode.length == 0){ return; } imageProgressNode.attr("value",progress); }; /** * @brief Notifies that the image upload as finished * * @param imageNodeIdentifier The unique image ID for the uploaded image */ ZSSEditor.markImageUploadDone = function(imageNodeIdentifier) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length > 0){ // remove identifier attributed from image imageNode.removeAttr('data-wpid'); // remove uploading style imageNode.removeClass("uploading"); imageNode.removeAttr("class"); // Remove all extra formatting nodes for progress if (imageNode.parent().attr("id") == this.getImageContainerIdentifier(imageNodeIdentifier)) { // remove id from container to avoid to report a user removal imageNode.parent().attr("id", ""); imageNode.parent().replaceWith(imageNode); } // Wrap link around image var linkTag = ''; imageNode.wrap(linkTag); } var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); ZSSEditor.callback("callback-input", joinedArguments); // We invoke the sendImageReplacedCallback with a delay to avoid for // it to be ignored by the webview because of the previous callback being done. var thisObj = this; setTimeout(function() { thisObj.sendImageReplacedCallback(imageNodeIdentifier);}, 500); }; /** * @brief Callbacks to native that the image upload as finished and the local url was replaced by the remote url * * @param imageNodeIdentifier The unique image ID for the uploaded image */ ZSSEditor.sendImageReplacedCallback = function( imageNodeIdentifier ) { var arguments = ['id=' + encodeURIComponent( imageNodeIdentifier )]; var joinedArguments = arguments.join( defaultCallbackSeparator ); this.callback("callback-image-replaced", joinedArguments); }; /** * @brief Marks the image as failed to upload * * @param imageNodeIdentifier This is a unique ID provided by the caller. * @param message A message to show to the user, overlayed on the image */ ZSSEditor.markImageUploadFailed = function(imageNodeIdentifier, message) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length == 0){ return; } var sizeClass = ''; if ( imageNode[0].width > 480 && imageNode[0].height > 240 ) { sizeClass = "largeFail"; } else if ( imageNode[0].width < 100 || imageNode[0].height < 100 ) { sizeClass = "smallFail"; } imageNode.addClass('failed'); var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier); if(imageContainerNode.length != 0){ imageContainerNode.attr("data-failed", message); imageNode.removeClass("uploading"); imageContainerNode.addClass('failed'); imageContainerNode.addClass(sizeClass); } var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier); if (imageProgressNode.length != 0){ imageProgressNode.addClass('failed'); } }; /** * @brief Unmarks the image as failed to upload * * @param imageNodeIdentifier This is a unique ID provided by the caller. */ ZSSEditor.unmarkImageUploadFailed = function(imageNodeIdentifier, message) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length != 0){ imageNode.removeClass('failed'); } var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier); if(imageContainerNode.length != 0){ imageContainerNode.removeAttr("data-failed"); imageContainerNode.removeClass('failed'); } var imageProgressNode = this.getImageProgressNodeWithIdentifier(imageNodeIdentifier); if (imageProgressNode.length != 0){ imageProgressNode.removeClass('failed'); } }; /** * @brief Remove the image from the DOM. * * @param imageNodeIdentifier This is a unique ID provided by the caller. */ ZSSEditor.removeImage = function(imageNodeIdentifier) { var imageNode = this.getImageNodeWithIdentifier(imageNodeIdentifier); if (imageNode.length != 0){ imageNode.remove(); } // if image is inside options container we need to remove the container var imageContainerNode = this.getImageContainerNodeWithIdentifier(imageNodeIdentifier); if (imageContainerNode.length != 0){ //reset id before removal to avoid detection of user removal imageContainerNode.attr("id",""); imageContainerNode.remove(); } }; /** * @brief Callbacks to native that the media container was deleted by the user * * @param mediaNodeIdentifier The unique media ID */ ZSSEditor.sendMediaRemovedCallback = function(mediaNodeIdentifier) { var arguments = ['id=' + encodeURIComponent(mediaNodeIdentifier)]; var joinedArguments = arguments.join(defaultCallbackSeparator); this.callback("callback-media-removed", joinedArguments); }; /** * @brief Inserts a video tag using the videoURL as source and posterURL as the * image to show while video is loading. * * @param videoURL the url of the video to present * @param posterURL the url of an image to show while the video is loading * @param alt the alt description when the video is not supported. * */ ZSSEditor.insertVideo = function(videoURL, posterURL, alt) { var html = ''; this.insertHTML(html); this.sendEnabledStyles(); }; /** * @brief Inserts a video tag marked with a identifier using only a poster image. Useful for videos that need to be uploaded. * @details By inserting a video with only a porter URL, we can make sure the video element is shown to the user * as soon as it's selected for uploading. Once the video is successfully uploaded * the application should call replaceLocalVideoWithRemoteVideo(). * * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as * a mechanism to update the video node with the remote URL * when replaceLocalVideoWithRemoteVideo() is called. * @param posterURL The URL of a poster image to display while the video is being uploaded. */ ZSSEditor.insertInProgressVideoWithIDUsingPosterImage = function(videoNodeIdentifier, posterURL) { var space = ' '; var progressIdentifier = this.getVideoProgressIdentifier(videoNodeIdentifier); var videoContainerIdentifier = this.getVideoContainerIdentifier(videoNodeIdentifier); var videoContainerStart = ''; var videoContainerEnd = ''; var progress = ''; var video = ''; var html = space + videoContainerStart + progress + video + videoContainerEnd + space; this.insertHTML(html); this.sendEnabledStyles(); }; ZSSEditor.getVideoNodeWithIdentifier = function(videoNodeIdentifier) { return $('video[data-wpid="' + videoNodeIdentifier+'"]'); }; ZSSEditor.getVideoProgressIdentifier = function(videoNodeIdentifier) { return 'progress_' + videoNodeIdentifier; }; ZSSEditor.getVideoProgressNodeWithIdentifier = function(videoNodeIdentifier) { return $('#'+this.getVideoProgressIdentifier(videoNodeIdentifier)); }; ZSSEditor.getVideoContainerIdentifier = function(videoNodeIdentifier) { return 'video_container_' + videoNodeIdentifier; }; ZSSEditor.getVideoContainerNodeWithIdentifier = function(videoNodeIdentifier) { return $('#'+this.getVideoContainerIdentifier(videoNodeIdentifier)); }; /** * @brief Replaces a local Video URL with a remote Video URL. Useful for videos that have * just finished uploading. * @details The remote Video can be available after a while, when uploading Videos. This method * allows for the remote URL to be loaded once the upload completes. * * @param videoNodeIdentifier This is a unique ID provided by the caller. It exists as * a mechanism to update the Video node with the remote URL * when replaceLocalVideoWithRemoteVideo() is called. * @param remoteVideoUrl The URL of the remote Video to display. * @param remotePosterUrl The URL of thre remote poster image to display * @param videopressID VideoPress Guid of the video if any */ ZSSEditor.replaceLocalVideoWithRemoteVideo = function(videoNodeIdentifier, remoteVideoUrl, remotePosterUrl, videopressID) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length == 0) { // even if the Video is not present anymore we must do callback this.markVideoUploadDone(videoNodeIdentifier); return; } videoNode.attr('src', remoteVideoUrl); videoNode.attr('controls', ''); if (videopressID != '') { videoNode.attr('data-wpvideopress', videopressID); } videoNode.attr('poster', remotePosterUrl); var thisObj = this; videoNode.on('webkitbeginfullscreen', function (event){ thisObj.sendVideoFullScreenStarted(); } ); videoNode.on('webkitendfullscreen', function (event){ thisObj.sendVideoFullScreenEnded(); } ); videoNode.on('error', function(event) { videoNode.load()} ); this.markVideoUploadDone(videoNodeIdentifier); }; /** * @brief Update the progress indicator for the Video identified with the value in progress. * * @param VideoNodeIdentifier This is a unique ID provided by the caller. * @param progress A value between 0 and 1 indicating the progress on the Video. */ ZSSEditor.setProgressOnVideo = function(videoNodeIdentifier, progress) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length == 0){ return; } if (progress < 1){ videoNode.addClass("uploading"); } var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); if (videoProgressNode.length == 0){ return; } videoProgressNode.attr("value",progress); }; /** * @brief Notifies that the Video upload as finished * * @param VideoNodeIdentifier The unique Video ID for the uploaded Video */ ZSSEditor.markVideoUploadDone = function(videoNodeIdentifier) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length > 0) { // remove identifier attributed from Video videoNode.removeAttr('data-wpid'); // remove uploading style videoNode.removeClass("uploading"); videoNode.removeAttr("class"); // Remove all extra formatting nodes for progress if (videoNode.parent().attr("id") == this.getVideoContainerIdentifier(videoNodeIdentifier)) { // remove id from container to avoid to report a user removal videoNode.parent().attr("id", ""); videoNode.parent().replaceWith(videoNode); } } var joinedArguments = ZSSEditor.getJoinedFocusedFieldIdAndCaretArguments(); ZSSEditor.callback("callback-input", joinedArguments); // We invoke the sendVideoReplacedCallback with a delay to avoid for // it to be ignored by the webview because of the previous callback being done. var thisObj = this; setTimeout(function() { thisObj.sendVideoReplacedCallback(videoNodeIdentifier);}, 500); }; /** * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url * * @param videoNodeIdentifier the unique video ID for the uploaded Video */ ZSSEditor.sendVideoReplacedCallback = function( videoNodeIdentifier ) { var arguments = ['id=' + encodeURIComponent( videoNodeIdentifier )]; var joinedArguments = arguments.join( defaultCallbackSeparator ); this.callback("callback-video-replaced", joinedArguments); }; /** * @brief Callbacks to native that the video entered full screen mode * */ ZSSEditor.sendVideoFullScreenStarted = function() { this.callback("callback-video-fullscreen-started", "empty"); }; /** * @brief Callbacks to native that the video entered full screen mode * */ ZSSEditor.sendVideoFullScreenEnded = function() { this.callback("callback-video-fullscreen-ended", "empty"); }; /** * @brief Callbacks to native that the video upload as finished and the local url was replaced by the remote url * * @param videoNodeIdentifier the unique video ID for the uploaded Video */ ZSSEditor.sendVideoPressInfoRequest = function( videoPressID ) { var arguments = ['id=' + encodeURIComponent( videoPressID )]; var joinedArguments = arguments.join( defaultCallbackSeparator ); this.callback("callback-videopress-info-request", joinedArguments); }; /** * @brief Marks the Video as failed to upload * * @param VideoNodeIdentifier This is a unique ID provided by the caller. * @param message A message to show to the user, overlayed on the Video */ ZSSEditor.markVideoUploadFailed = function(videoNodeIdentifier, message) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length == 0){ return; } var sizeClass = ''; if ( videoNode[0].width > 480 && videoNode[0].height > 240 ) { sizeClass = "largeFail"; } else if ( videoNode[0].width < 100 || videoNode[0].height < 100 ) { sizeClass = "smallFail"; } videoNode.addClass('failed'); var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); if(videoContainerNode.length != 0){ videoContainerNode.attr("data-failed", message); videoNode.removeClass("uploading"); videoContainerNode.addClass('failed'); videoContainerNode.addClass(sizeClass); } var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); if (videoProgressNode.length != 0){ videoProgressNode.addClass('failed'); } }; /** * @brief Unmarks the Video as failed to upload * * @param VideoNodeIdentifier This is a unique ID provided by the caller. */ ZSSEditor.unmarkVideoUploadFailed = function(videoNodeIdentifier, message) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length != 0){ videoNode.removeClass('failed'); } var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); if(videoContainerNode.length != 0){ videoContainerNode.removeAttr("data-failed"); videoContainerNode.removeClass('failed'); } var videoProgressNode = this.getVideoProgressNodeWithIdentifier(videoNodeIdentifier); if (videoProgressNode.length != 0){ videoProgressNode.removeClass('failed'); } }; /** * @brief Remove the Video from the DOM. * * @param videoNodeIdentifier This is a unique ID provided by the caller. */ ZSSEditor.removeVideo = function(videoNodeIdentifier) { var videoNode = this.getVideoNodeWithIdentifier(videoNodeIdentifier); if (videoNode.length != 0){ videoNode.remove(); } // if Video is inside options container we need to remove the container var videoContainerNode = this.getVideoContainerNodeWithIdentifier(videoNodeIdentifier); if (videoContainerNode.length != 0){ //reset id before removal to avoid detection of user removal videoContainerNode.attr("id",""); videoContainerNode.remove(); } }; ZSSEditor.replaceVideoPressVideosForShortcode = function ( html) { // call methods to restore any transformed content from its visual presentation to its source code. var regex = /]*data-wpvideopress="([\s\S]+?)"[^>]*>*<\/video>/g; var str = html.replace( regex, ZSSEditor.removeVideoVisualFormattingCallback ); return str; } ZSSEditor.removeVideoVisualFormattingCallback = function( match, content ) { return "[wpvideo " + content + "]"; } ZSSEditor.applyVideoFormattingCallback = function( match ) { if (match.attrs.numeric.length == 0) { return match.content; } var videopressID = match.attrs.numeric[0]; var posterSVG = '"wpposter.svg"'; // The empty 'onclick' is important. It prevents the cursor jumping to the end // of the content body when `-webkit-user-select: none` is set and the video is tapped. var out = ''; return out; } /** * @brief Sets the VidoPress video URL and poster URL on a video tag. * @details When switching between source and visual the wpvideo shortcode are replace by a video tag. Unfortunaly there * is no way to infer the video url from the shortcode so we need to find this information and then set it on the video tag. * * @param videopressID videopress identifier of the video. * @param videoURL URL of the video file to display. * @param posterURL URL of the poster image to display */ ZSSEditor.setVideoPressLinks = function(videopressID, videoURL, posterURL ) { var videoNode = $('video[data-wpvideopress="' + videopressID+'"]'); if (videoNode.length == 0) { return; } videoNode.attr('src', videoURL); videoNode.attr('controls', ''); if (posterURL.length == 0) { videoNode.attr('poster', 'wpposter.svg'); } else { videoNode.attr('poster', posterURL); } var thisObj = this; videoNode.on('webkitbeginfullscreen', function (event){ thisObj.sendVideoFullScreenStarted(); } ); videoNode.on('webkitendfullscreen', function (event){ thisObj.sendVideoFullScreenEnded(); } ); videoNode.load(); }; /** * @brief Stops all video of playing * */ ZSSEditor.pauseAllVideos = function () { $('video').each(function() { this.pause(); }); } /** * @brief Updates the currently selected image, replacing its markup with * new markup based on the specified meta data string. * * @param imageMetaString A JSON string representing the updated meta data. */ ZSSEditor.updateCurrentImageMeta = function( imageMetaString ) { if ( !ZSSEditor.currentEditingImage ) { return; } var imageMeta = JSON.parse( imageMetaString ); var html = ZSSEditor.createImageFromMeta( imageMeta ); // Insert the updated html and remove the outdated node. // This approach is preferred to selecting the current node via a range, // and then replacing it when calling insertHTML. The insertHTML call can, // in certain cases, modify the current and inserted markup depending on what // elements surround the targeted node. This approach is safer. var node = ZSSEditor.findImageCaptionNode( ZSSEditor.currentEditingImage ); node.insertAdjacentHTML( 'afterend', html ); node.remove(); ZSSEditor.currentEditingImage = null; } // 选择图片后的操作, 添加overlay // 为什么有blur的感觉? ZSSEditor.applyImageSelectionFormatting = function( imageNode ) { var node = ZSSEditor.findImageCaptionNode( imageNode ); var sizeClass = ""; if ( imageNode.width < 100 || imageNode.height < 100 ) { sizeClass = " small"; } var overlay = 'Edit'; var html = '' + overlay + ''; node.insertAdjacentHTML( 'beforebegin', html ); var selectionNode = node.previousSibling; selectionNode.appendChild( node ); } ZSSEditor.removeImageSelectionFormatting = function( imageNode ) { var node = ZSSEditor.findImageCaptionNode( imageNode ); if ( !node.parentNode || node.parentNode.className.indexOf( "edit-container" ) == -1 ) { return; } var parentNode = node.parentNode; var container = parentNode.parentNode; container.insertBefore( node, parentNode ); parentNode.remove(); } ZSSEditor.removeImageSelectionFormattingFromHTML = function( html ) { var tmp = document.createElement( "div" ); var tmpDom = $( tmp ).html( html ); var matches = tmpDom.find( "span.edit-container img" ); if ( matches.length == 0 ) { return html; } for ( var i = 0; i < matches.length; i++ ) { ZSSEditor.removeImageSelectionFormatting( matches[i] ); } return tmpDom.html(); } /** * @brief Finds all related caption nodes for the specified image node. * * @param imageNode An image node in the DOM to inspect. */ ZSSEditor.findImageCaptionNode = function( imageNode ) { var node = imageNode; if ( node.parentNode && node.parentNode.nodeName === 'A' ) { node = node.parentNode; } if ( node.parentNode && node.parentNode.className.indexOf( 'wp-caption' ) != -1 ) { node = node.parentNode; } if ( node.parentNode && (node.parentNode.className.indexOf( 'wp-temp' ) != -1 ) ) { node = node.parentNode; } return node; } /** * Modified from wp-includes/js/media-editor.js * see `image` * * @brief Construct html markup for an image, and optionally a link an caption shortcode. * * @param props A dictionary of properties used to compose the markup. See comments in extractImageMeta. * * @return Returns the html mark up as a string */ ZSSEditor.createImageFromMeta = function( props ) { var img = {}, options, classes, shortcode, html; classes = props.classes || []; if ( ! ( classes instanceof Array ) ) { classes = classes.split( ' ' ); } _.extend( img, _.pick( props, 'width', 'height', 'alt', 'src', 'title' ) ); // Only assign the align class to the image if we're not printing // a caption, since the alignment is sent to the shortcode. if ( props.align && ! props.caption ) { classes.push( 'align' + props.align ); } if ( props.size ) { classes.push( 'size-' + props.size ); } if ( props.attachment_id ) { classes.push( 'wp-image-' + props.attachment_id ); } img['class'] = _.compact( classes ).join(' '); // Generate `img` tag options. options = { tag: 'img', attrs: img, single: true }; // Generate the `a` element options, if they exist. if ( props.linkUrl ) { options = { tag: 'a', attrs: { href: props.linkUrl }, content: options }; if ( props.linkClassName ) { options.attrs.class = props.linkClassName; } if ( props.linkRel ) { options.attrs.rel = props.linkRel; } if ( props.linkTargetBlank ) { // expects a boolean options.attrs.target = "_blank"; } } html = wp.html.string( options ); // Generate the caption shortcode. if ( props.caption ) { shortcode = {}; if ( img.width ) { shortcode.width = img.width; } if ( props.captionId ) { shortcode.id = props.captionId; } if ( props.align ) { shortcode.align = 'align' + props.align; } else { shortcode.align = 'alignnone'; } if (props.captionClassName) { shortcode.class = props.captionClassName; } html = wp.shortcode.string({ tag: 'caption', attrs: shortcode, content: html + ' ' + props.caption }); html = ZSSEditor.applyVisualFormatting( html ); } return html; }; /** * Modified from wp-includes/js/tinymce/plugins/wpeditimage/plugin.js * see `extractImageData` * * @brief Extracts properties and meta data from an image, and optionally its link and caption. * * @param imageNode An image node in the DOM to inspect. * * @return Returns an object containing the extracted properties and meta data. */ ZSSEditor.extractImageMeta = function( imageNode ) { var classes, extraClasses, metadata, captionBlock, caption, link, width, height, captionClassName = [], isIntRegExp = /^\d+$/; // Default attributes. All values are strings, except linkTargetBlank metadata = { align: 'none', // Accepted values: center, left, right or empty string. alt: '', // Image alt attribute attachment_id: '', // Numeric attachment id of the image in the site's media library caption: '', // The text of the caption for the image (if any) captionClassName: '', // The classes for the caption shortcode (if any). captionId: '', // The caption shortcode's ID attribute. The numeric value should match the value of attachment_id classes: '', // The class attribute for the image. Does not include editor generated classes height: '', // The image height attribute linkClassName: '', // The class attribute for the link linkRel: '', // The rel attribute for the link (if any) linkTargetBlank: false, // true if the link should open in a new window. linkUrl: '', // The href attribute of the link size: 'custom', // Accepted values: custom, medium, large, thumbnail, or empty string src: '', // The src attribute of the image title: '', // The title attribute of the image (if any) width: '', // The image width attribute naturalWidth:'', // The natural width of the image. naturalHeight:'' // The natural height of the image. }; // populate metadata with values of matched attributes metadata.src = $( imageNode ).attr( 'src' ) || ''; metadata.alt = $( imageNode ).attr( 'alt' ) || ''; metadata.title = $( imageNode ).attr( 'title' ) || ''; metadata.naturalWidth = imageNode.naturalWidth; metadata.naturalHeight = imageNode.naturalHeight; width = $(imageNode).attr( 'width' ); height = $(imageNode).attr( 'height' ); if ( ! isIntRegExp.test( width ) || parseInt( width, 10 ) < 1 ) { width = imageNode.naturalWidth || imageNode.width; } if ( ! isIntRegExp.test( height ) || parseInt( height, 10 ) < 1 ) { height = imageNode.naturalHeight || imageNode.height; } metadata.width = width; metadata.height = height; classes = imageNode.className.split( /\s+/ ); extraClasses = []; $.each( classes, function( index, value ) { if ( /^wp-image/.test( value ) ) { metadata.attachment_id = parseInt( value.replace( 'wp-image-', '' ), 10 ); } else if ( /^align/.test( value ) ) { metadata.align = value.replace( 'align', '' ); } else if ( /^size/.test( value ) ) { metadata.size = value.replace( 'size-', '' ); } else { extraClasses.push( value ); } } ); metadata.classes = extraClasses.join( ' ' ); // Extract caption var captionMeta = ZSSEditor.captionMetaForImage( imageNode ) metadata = $.extend( metadata, captionMeta ); // Extract linkTo if ( imageNode.parentNode && imageNode.parentNode.nodeName === 'A' ) { link = imageNode.parentNode; metadata.linkClassName = link.className; metadata.linkRel = $( link ).attr( 'rel' ) || ''; metadata.linkTargetBlank = $( link ).attr( 'target' ) === '_blank' ? true : false; metadata.linkUrl = $( link ).attr( 'href' ) || ''; } return metadata; }; /** * @brief Extracts the caption shortcode for an image. * * @param imageNode An image node in the DOM to inspect. * * @return Returns a shortcode match (if any) for the passed image node. * See shortcode.js::next for details */ ZSSEditor.getCaptionForImage = function( imageNode ) { var node = ZSSEditor.findImageCaptionNode( imageNode ); // Ensure we're working with the formatted caption if ( node.className.indexOf( 'wp-temp' ) == -1 ) { return; } var html = node.outerHTML; html = ZSSEditor.removeVisualFormatting( html ); return wp.shortcode.next( "caption", html, 0 ); }; /** * @brief Extracts meta data for the caption (if any) for the passed image node. * * @param imageNode An image node in the DOM to inspect. * * @return Returns an object containing the extracted meta data. * See shortcode.js::next or details */ ZSSEditor.captionMetaForImage = function( imageNode ) { var attrs, meta = { align: '', caption: '', captionClassName: '', captionId: '' }; var caption = ZSSEditor.getCaptionForImage( imageNode ); if ( !caption ) { return meta; } attrs = caption.shortcode.attrs.named; if ( attrs.align ) { meta.align = attrs.align.replace( 'align', '' ); } if ( attrs.class ) { meta.captionClassName = attrs.class; } if ( attrs.id ) { meta.captionId = attrs.id; } meta.caption = caption.shortcode.content.substr( caption.shortcode.content.lastIndexOf( ">" ) + 1 ); return meta; } /** * @brief Adds visual formatting to a caption shortcodes. * * @param html The markup containing caption shortcodes to process. * * @return The html with caption shortcodes replaced with editor specific markup. * See shortcode.js::next or details */ ZSSEditor.applyCaptionFormatting = function( match ) { var attrs = match.attrs.named; // The empty 'onclick' is important. It prevents the cursor jumping to the end // of the content body when `-webkit-user-select: none` is set and the caption is tapped. var out = '