Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion Aztec/Classes/Libxml2/DOMString.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ extension Libxml2 {
}
}

/// Disables an image from the specified range.
/// Removes an image from the specified range.
///
/// - Parameters:
/// - range: the range to remove the style from.
Expand All @@ -280,6 +280,17 @@ extension Libxml2 {
}
}

/// Removes an video from the specified range.
///
/// - Parameters:
/// - range: the range to remove the style from.
///
func removeVideo(spanning range: NSRange) {
performAsyncUndoable { [weak self] in
self?.removeVideoSynchronously(spanning: range)
}
}

/// Disables italic from the specified range.
///
/// - Parameters:
Expand Down Expand Up @@ -357,6 +368,10 @@ extension Libxml2 {
private func removeImageSynchronously(spanning range: NSRange) {
domEditor.unwrap(range: range, fromElementsNamed: StandardElementType.img.equivalentNames)
}

private func removeVideoSynchronously(spanning range: NSRange) {
domEditor.unwrap(range: range, fromElementsNamed: StandardElementType.video.equivalentNames)
}

private func removeItalicSynchronously(spanning range: NSRange) {
domEditor.unwrap(range: range, fromElementsNamed: StandardElementType.i.equivalentNames)
Expand Down Expand Up @@ -524,6 +539,33 @@ extension Libxml2 {
rootNode.replaceCharacters(in: range, with: descriptor)
}

// MARK: - Videos

/// Replaces the specified range with a given image.
///
/// - Parameters:
/// - range: the range to insert the image
/// - videoURL: the URL for the video src attribute
/// - posterURL: the URL for ther video poster attribute
///
func replace(_ range: NSRange, withVideoURL videoURL: URL, posterURL: URL?) {
performAsyncUndoable { [weak self] in
self?.replaceSynchronously(range, withVideoURL: videoURL, posterURL: posterURL)
}
}

private func replaceSynchronously(_ range: NSRange, withVideoURL videoURL: URL, posterURL: URL?) {
let videoURLString = videoURL.absoluteString

var attributes = [Libxml2.StringAttribute(name:"src", value: videoURLString)]
if let posterURLString = posterURL?.absoluteString {
attributes.append(Libxml2.StringAttribute(name:"poster", value: posterURLString))
}
let descriptor = ElementNodeDescriptor(elementType: .video, attributes: attributes)

rootNode.replaceCharacters(in: range, with: descriptor)
}

/// Replaces the specified range with a Horizontal Ruler Style.
///
/// - Parameters:
Expand Down
4 changes: 3 additions & 1 deletion Aztec/Classes/TextKit/MediaAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ open class MediaAttachment: NSTextAttachment
let padding = textContainer?.lineFragmentPadding ?? 0
let width = lineFrag.width - padding * 2

return CGRect(origin: CGPoint.zero, size: CGSize(width: width, height: onScreenHeight(width)))
let size = CGSize(width: width, height: onScreenHeight(width))

return CGRect(origin: CGPoint.zero, size: size)
}

func updateImage(inTextContainer textContainer: NSTextContainer? = nil) {
Expand Down
62 changes: 59 additions & 3 deletions Aztec/Classes/TextKit/TextStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ open class TextStorage: NSTextStorage {
let isCommentAttachment = sourceValue is CommentAttachment || targetValue is CommentAttachment
let isHtmlAttachment = sourceValue is HTMLAttachment || targetValue is HTMLAttachment
let isLineAttachment = sourceValue is LineAttachment || targetValue is LineAttachment
let isImageAttachment = sourceValue is ImageAttachment || targetValue is ImageAttachment
let isVideoAttachment = sourceValue is VideoAttachment || targetValue is VideoAttachment

switch(key) {
case NSFontAttributeName:
Expand Down Expand Up @@ -403,11 +405,16 @@ open class TextStorage: NSTextStorage {
let targetAttachment = targetValue as? HTMLAttachment

processHtmlAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
case NSAttachmentAttributeName:
case NSAttachmentAttributeName where isImageAttachment:
let sourceAttachment = sourceValue as? ImageAttachment
let targetAttachment = targetValue as? ImageAttachment

processAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
processImageAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
case NSAttachmentAttributeName where isVideoAttachment:
let sourceAttachment = sourceValue as? VideoAttachment
let targetAttachment = targetValue as? VideoAttachment

processVideoAttachmentDifferences(in: domRange, betweenOriginal: sourceAttachment, andNew: targetAttachment)
case NSParagraphStyleAttributeName:
let sourceStyle = sourceValue as? ParagraphStyle
let targetStyle = targetValue as? ParagraphStyle
Expand Down Expand Up @@ -466,7 +473,7 @@ open class TextStorage: NSTextStorage {
/// - original: the original attachment existing in the range if any.
/// - new: the new attachment to apply to the range if any.
///
private func processAttachmentDifferences(in range: NSRange, betweenOriginal original: ImageAttachment?, andNew new: ImageAttachment?) {
private func processImageAttachmentDifferences(in range: NSRange, betweenOriginal original: ImageAttachment?, andNew new: ImageAttachment?) {

let originalUrl = original?.url
let newUrl = new?.url
Expand All @@ -486,6 +493,33 @@ open class TextStorage: NSTextStorage {
}
}

/// Process difference in attachmente properties, and applies them to the DOM in the specified range
///
/// - Parameters:
/// - range: the range in the DOM where the differences must be applied.
/// - original: the original attachment existing in the range if any.
/// - new: the new attachment to apply to the range if any.
///
private func processVideoAttachmentDifferences(in range: NSRange, betweenOriginal original: VideoAttachment?, andNew new: VideoAttachment?) {

let originalUrl = original?.srcURL
let newUrl = new?.srcURL

let addVideoUrl = originalUrl == nil && newUrl != nil
let removeVideoUrl = originalUrl != nil && newUrl == nil

if addVideoUrl {
guard let urlToAdd = newUrl else {
assertionFailure("This should not be possible. Review your logic.")
return
}

dom.replace(range, withVideoURL: urlToAdd, posterURL: new?.posterURL)
} else if removeVideoUrl {
dom.removeVideo(spanning: range)
}
}

private func processLineAttachmentDifferences(in range: NSRange, betweenOriginal original: LineAttachment?, andNew new: LineAttachment?) {

dom.replaceWithHorizontalRuler(range)
Expand Down Expand Up @@ -747,6 +781,28 @@ open class TextStorage: NSTextStorage {
return attachment
}

/// Insert Video Element at the specified range using url as source
///
/// - parameter sourceURL: the source URL of the video
/// - parameter posterURL: an URL pointing to a frame/thumbnail of the video
/// - parameter position: the position to insert the image
/// - parameter placeHolderImage: an image to display while the image from sourceURL is being prepared
///
/// - returns: the attachment object that was created and inserted on the text
///
func insertVideo(sourceURL: URL, posterURL: URL?, atPosition position:Int, placeHolderImage: UIImage, identifier: String = UUID().uuidString) -> VideoAttachment {
let attachment = VideoAttachment(identifier: identifier, srcURL: sourceURL, posterURL: posterURL)
attachment.delegate = self
attachment.image = placeHolderImage

// Inject the Attachment and Layout
let insertionRange = NSMakeRange(position, 0)
let attachmentString = NSAttributedString(attachment: attachment)
replaceCharacters(in: insertionRange, with: attachmentString)

return attachment
}

/// Insert an HR element at the specifice range
///
/// - Parameter range: the range where the element will be inserted
Expand Down
35 changes: 28 additions & 7 deletions Aztec/Classes/TextKit/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -973,26 +973,47 @@ open class TextView: UITextView {
/// Inserts a Video attachment at the specified index
///
/// - Parameters:
/// - index: The character index at which to insert the image.
/// - params: TBD
/// - location: the location in the text to insert the video
/// - sourceURL: the video source URL
/// - posterURL: the video poster image URL
/// - placeHolderImage: an image to use has an placeholder while the video poster is being loaded
/// - identifier: an unique indentifier for the video
///
open func insertVideo(_ index: Int, params: [String: AnyObject]) {
print("video")
/// - Returns: the video attachment object that was inserted.
///
open func insertVideo(atLocation location: Int, sourceURL: URL, posterURL: URL?, placeHolderImage: UIImage?, identifier: String = UUID().uuidString) -> VideoAttachment {
let attachment = storage.insertVideo(sourceURL: sourceURL, posterURL: posterURL, atPosition: location, placeHolderImage: placeHolderImage ?? defaultMissingImage, identifier: identifier)
let length = NSAttributedString.lengthOfTextAttachment
textStorage.addAttributes(typingAttributes, range: NSMakeRange(location, length))
selectedRange = NSMakeRange(location+length, 0)
delegate?.textViewDidChange?(self)
return attachment
}

/// Returns the associated TextAttachment, at a given point, if any.
/// Returns the associated NSTextAttachment, at a given point, if any.
///
/// - Parameter point: The point on screen to check for attachments.
///
/// - Returns: The associated TextAttachment.
/// - Returns: The associated NSTextAttachment.
///
open func attachmentAtPoint(_ point: CGPoint) -> NSTextAttachment? {
let index = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard index < textStorage.length else {
return nil
}

return textStorage.attribute(NSAttachmentAttributeName, at: index, effectiveRange: nil) as? NSTextAttachment
guard let attachment = textStorage.attribute(NSAttachmentAttributeName, at: index, effectiveRange: nil) as? NSTextAttachment else {
return nil
}

let glyphIndex = layoutManager.glyphIndexForCharacter(at: index)
let bounds = layoutManager.boundingRect(forGlyphRange: NSRange(location: glyphIndex, length: 1), in: textContainer)

if bounds.contains(point) {
return attachment
}

return nil
}

/// Move the selected range to the nearest character of the point specified in the textView
Expand Down
4 changes: 2 additions & 2 deletions Aztec/Classes/TextKit/VideoAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ open class VideoAttachment: MediaAttachment
///
open var posterURL: URL? {
get {
return self.srcURL
return self.url
}

set {
self.srcURL = newValue
self.url = newValue
}
}

Expand Down
62 changes: 51 additions & 11 deletions Example/Example/EditorDemoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -789,14 +789,15 @@ extension EditorDemoController: TextViewMediaDelegate {
}

if let videoAttachment = attachment as? VideoAttachment {
if let imageAttachment = currentSelectedAttachment {
deselected(textAttachment: imageAttachment, atPosition: position)
}
selected(videoAttachment: videoAttachment, atPosition: position)
}
}

func textView(_ textView: TextView, deselectedAttachment attachment: NSTextAttachment, atPosition position: CGPoint) {
if let imgAttachment = attachment as? ImageAttachment {
deselected(textAttachment: imgAttachment, atPosition: position)
}
deselected(textAttachment: attachment, atPosition: position)
}

func selected(textAttachment attachment: ImageAttachment, atPosition position: CGPoint) {
Expand All @@ -817,9 +818,12 @@ extension EditorDemoController: TextViewMediaDelegate {
}
}

func deselected(textAttachment attachment: ImageAttachment, atPosition position: CGPoint) {
attachment.clearAllOverlays()
richTextView.refreshLayoutFor(attachment: attachment)
func deselected(textAttachment attachment: NSTextAttachment, atPosition position: CGPoint) {
currentSelectedAttachment = nil
if let mediaAttachment = attachment as? MediaAttachment {
mediaAttachment.clearAllOverlays()
richTextView.refreshLayoutFor(attachment: mediaAttachment)
}
}

func selected(videoAttachment attachment: VideoAttachment, atPosition position: CGPoint) {
Expand Down Expand Up @@ -851,14 +855,30 @@ extension EditorDemoController: UIImagePickerControllerDelegate
{
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
dismiss(animated: true, completion: nil)

guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
richTextView.becomeFirstResponder()
guard let mediaType = info[UIImagePickerControllerMediaType] as? String else {
return
}
let typeImage = kUTTypeImage as String
let typeMovie = kUTTypeMovie as String

// Insert Image + Reclaim Focus
insertImage(image)
richTextView.becomeFirstResponder()
switch mediaType {
case typeImage:
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
return
}

// Insert Image + Reclaim Focus
insertImage(image)

case typeMovie:
guard let videoURL = info[UIImagePickerControllerMediaURL] as? URL else {
return
}
insertVideo(videoURL)
default:
print("Media type not supported: \(mediaType)")
}
}
}

Expand Down Expand Up @@ -895,6 +915,26 @@ private extension EditorDemoController
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(EditorDemoController.timerFireMethod(_:)), userInfo: progress, repeats: true)
}

func insertVideo(_ videoURL: URL) {

let index = richTextView.positionForCursor()

let asset = AVURLAsset(url: videoURL, options: nil)
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
guard let cgImage = try? imgGenerator.copyCGImage(at: CMTimeMake(0, 1), actualTime: nil) else {
return
}
let posterImage = UIImage(cgImage: cgImage)
let posterURL = saveToDisk(image: posterImage)
let attachment = richTextView.insertVideo(atLocation: index, sourceURL: videoURL, posterURL: posterURL, placeHolderImage: posterImage)
let imageID = attachment.identifier
let progress = Progress(parent: nil, userInfo: ["imageID": imageID])
progress.totalUnitCount = 100

Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(EditorDemoController.timerFireMethod(_:)), userInfo: progress, repeats: true)
}

@objc func timerFireMethod(_ timer: Timer) {
guard let progress = timer.userInfo as? Progress,
let imageId = progress.userInfo[ProgressUserInfoKey("imageID")] as? String
Expand Down